package com.mg.framework.tiled;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import android.content.res.AssetManager;

public class TMXMapReader {

	private EntityResolver entityResolver;
	private Map map;
	private AssetManager assetManager;
	private String basePath;
	private String tileObjectPackageName;

	public TMXMapReader(AssetManager assetManager) {
		this.entityResolver = new MapEntityResolver();
		this.assetManager = assetManager;
	}

	public Map readMap(String basePath, InputStream in, String tileObjectPackageName) throws Exception {
		this.map = null;
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		Document doc = null;
		factory.setIgnoringComments(true);
		factory.setIgnoringElementContentWhitespace(true);
		factory.setExpandEntityReferences(false);
		DocumentBuilder builder = factory.newDocumentBuilder();
		builder.setEntityResolver(entityResolver);
		InputSource insrc = new InputSource(in);
		doc = builder.parse(insrc);
		this.basePath = basePath;
		this.tileObjectPackageName = tileObjectPackageName;
		this.buildMap(doc);
		return map;
	}

	private void buildMap(Document doc) throws Exception {
		Node mapNode = null;
		Node item = null;
		mapNode = doc.getDocumentElement();

		if (!"map".equals(mapNode.getNodeName())) {
			throw new Exception("Not a valid tmx map file.");
		}

		// Get the map dimensions and create the map
		int mapWidth = getAttribute(mapNode, "width", 0);
		int mapHeight = getAttribute(mapNode, "height", 0);

		if (mapWidth > 0 && mapHeight > 0) {
			map = new Map(mapWidth, mapHeight);
		} else {
			throw new Exception("Couldn't locate map dimensions.");
		}

		map.setTileWidth(getAttribute(mapNode, "tilewidth", 0));
		map.setTileHeight(getAttribute(mapNode, "tileheight", 0));

		String orientation = getAttributeValue(mapNode, "orientation");
		if (orientation != null) {
			setOrientation(orientation);
		} else {
			throw new Exception("Couldn't locate map orientation");
		}

		// Load properties
		readProperties(mapNode.getChildNodes(), map.getProperties());

		// Load tilesets
		NodeList l = doc.getElementsByTagName("tileset");
		for (int i = 0; (item = l.item(i)) != null; i++) {
			map.addTileset(unmarshalTileset(item));
		}

		// Load the layers and objectgroups
		for (Node sibs = mapNode.getFirstChild(); sibs != null; sibs = sibs
				.getNextSibling()) {
			if ("layer".equals(sibs.getNodeName())) {
				MapLayer layer = readLayer(sibs);
				if (layer != null) {
					map.addLayer(layer);
				}
			} else if ("objectgroup".equals(sibs.getNodeName())) {
				MapLayer layer = readObjectGroup(sibs);
				if (layer != null) {
					map.addLayer(layer);
				}
			}
		}
	}

	private MapLayer readObjectGroup(Node t) {

		final int layerWidth = getAttribute(t, "width", map.getWidth());
		final int layerHeight = getAttribute(t, "height", map.getHeight());

		ObjectGroup og = new ObjectGroup(layerWidth, layerHeight);
		og.setName(getAttributeValue(t, "name"));

		// Add all objects from the objects group
		NodeList children = t.getChildNodes();

		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if ("object".equalsIgnoreCase(child.getNodeName())) {
				og.addObject(readMapObject(child));
			}
		}

		Properties props = new Properties();
		readProperties(children, props);
		og.setProperties(props);

		return og;
	}

	private MapObject readMapObject(Node t) {
		final String name = getAttributeValue(t, "name");
		final String type = getAttributeValue(t, "type");
		final int x = getAttribute(t, "x", 0);
		final int y = getAttribute(t, "y", 0);
		final int width = getAttribute(t, "width", 0);
		final int height = getAttribute(t, "height", 0);

		MapObject obj = new MapObject(x, y, width, height);
		if (name != null)
			obj.setName(name);
		if (type != null)
			obj.setType(type);

		NodeList children = t.getChildNodes();
		Properties props = new Properties();
		readProperties(children, props);
		obj.setProperties(props);
		return obj;
	}

	private TileSet unmarshalTileset(Node t) throws Exception {
		int firstGid = getAttribute(t, "firstgid", 1);
		final int tileWidth = getAttribute(t, "tilewidth",
				map != null ? map.getTileWidth() : 0);
		final int tileHeight = getAttribute(t, "tileheight",
				map != null ? map.getTileHeight() : 0);
		final int tileSpacing = getAttribute(t, "spacing", 0);
		final int tileMargin = getAttribute(t, "margin", 0);

		TileSet set = new TileSet();

		set.setName(getAttributeValue(t, "name"));
		set.setFirstGid(firstGid);

		boolean hasTilesetImage = false;
		NodeList children = t.getChildNodes();

		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);

			if (child.getNodeName().equalsIgnoreCase("image")) {

				if (hasTilesetImage) {
					continue;
				}

				String imgSource = getAttributeValue(child, "source");
				String transStr = getAttributeValue(child, "trans");

				if (imgSource != null) {
					hasTilesetImage = true;
					if (transStr != null) {
						if (transStr.startsWith("#"))
							transStr = transStr.substring(1);

						set.setTransparentColor(Integer.parseInt(transStr, 16));
					}

					InputStream in = this.assetManager.open(this.basePath
							+ imgSource);
					set.importTileBitmap(imgSource, in, new BasicTileCutter(
							tileWidth, tileHeight, tileSpacing, tileMargin));
				}
			} else if (child.getNodeName().equalsIgnoreCase("tile")) {
				Tile tile = unmarshalTile(set, child);
				Tile myTile = set.getTile(tile.getId());
				myTile.setProperties(tile.getProperties());
			}
		}
		return set;
	}

	private Tile unmarshalTile(TileSet set, Node t) throws Exception {
		Tile tile = new Tile();
		tile.setId(getAttribute(t, "id", -1));
		NodeList children = t.getChildNodes();
		tile.setTileSet(set);
		readProperties(children, tile.getProperties());
		return tile;
	}

	private MapLayer readLayer(Node t) throws Exception {
		final int layerWidth = getAttribute(t, "width", map.getWidth());
		final int layerHeight = getAttribute(t, "height", map.getHeight());

		TileLayer tl = new TileLayer(layerWidth, layerHeight);
		tl.setName(getAttributeValue(t, "name"));

		readProperties(t.getChildNodes(), tl.getProperties());

		for (Node child = t.getFirstChild(); child != null; child = child
				.getNextSibling()) {
			String nodeName = child.getNodeName();
			if ("data".equalsIgnoreCase(nodeName)) {
				String encoding = getAttributeValue(child, "encoding");

				if (encoding != null && "base64".equalsIgnoreCase(encoding)) {
					Node cdata = child.getFirstChild();
					if (cdata != null) {
						char[] enc = cdata.getNodeValue().trim().toCharArray();
						byte[] dec = Base64.decode(enc);
						ByteArrayInputStream bais = new ByteArrayInputStream(
								dec);
						InputStream is;

						String comp = getAttributeValue(child, "compression");

						if ("gzip".equalsIgnoreCase(comp)) {
							is = new GZIPInputStream(bais);
						} else if ("zlib".equalsIgnoreCase(comp)) {
							is = new InflaterInputStream(bais);
						} else {
							is = bais;
						}

						for (int y = 0; y < tl.getHeight(); y++) {
							for (int x = 0; x < tl.getWidth(); x++) {
								int tileId = 0;
								tileId |= is.read();
								tileId |= is.read() << 8;
								tileId |= is.read() << 16;
								tileId |= is.read() << 24;

								TileSet ts = map.findTileSetForTileGID(tileId);
								if (ts != null) {
									tl.setTileAt(
											x,
											y,
											ts.getTile(tileId
													- ts.getFirstGid()),this.tileObjectPackageName);
								} else {
									tl.setTileAt(x, y, null, null);
								}
							}
						}
					}
				}
			}
		}
		return tl;
	}

	private void readProperties(NodeList children, Properties props) {
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if ("property".equalsIgnoreCase(child.getNodeName())) {
				String key = getAttributeValue(child, "name");
				String value = getAttributeValue(child, "value");
				if (value == null) {
					Node grandChild = child.getFirstChild();
					if (grandChild != null) {
						value = grandChild.getNodeValue();
						if (value != null)
							value = value.trim();
					}
				}
				if (value != null)
					props.setProperty(key, value);
			} else if ("properties".equals(child.getNodeName())) {
				readProperties(child.getChildNodes(), props);
			}
		}
	}

	private int getAttribute(Node node, String attribname, int def) {
		final String attr = getAttributeValue(node, attribname);
		if (attr != null) {
			return Integer.parseInt(attr);
		} else {
			return def;
		}
	}

	private String getAttributeValue(Node node, String attribname) {
		final NamedNodeMap attributes = node.getAttributes();
		String value = null;
		if (attributes != null) {
			Node attribute = attributes.getNamedItem(attribname);
			if (attribute != null) {
				value = attribute.getNodeValue();
			}
		}
		return value;
	}

	private void setOrientation(String o) throws Exception {
		if ("isometric".equalsIgnoreCase(o)) {
			map.setOrientation(Map.ORIENTATION_ISOMETRIC);
		} else if ("orthogonal".equalsIgnoreCase(o)) {
			map.setOrientation(Map.ORIENTATION_ORTHOGONAL);
		} else {
			throw new Exception("Unknown orientation");
		}
	}

	private class MapEntityResolver implements EntityResolver {

		@Override
		public InputSource resolveEntity(String publicId, String systemId)
				throws SAXException, IOException {
			if (systemId.equals("http://mapeditor.org/dtd/1.0/map.dtd")) {
				return new InputSource(
						TMXMapReader.class
								.getResourceAsStream("resources/map.dtd"));
			}
			return null;
		}

	}
}
